package servers;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import security.*;
import servers.IRegisterServer;

import fanzone.EventInfo;
import fanzone.ResourceInfo;

public class Server implements IServer, Serializable {

	private static final long serialVersionUID = 1L;
	private static final String SERVER_FILE = "primary_server.data";
	private static final String REGISTER_SERVER = "RegisterServer";

	private String registerServerAddress;

	private String id;
	private boolean primary; // if server is primary
	private long version;

	private Map<String, CommunityInfo> communities; // name, community
	private Map<String, List<String>> users; // community, user list
	private Map<String, List<EventInfo>> events; // community, event list
	private Map<String, ResourceInfo> resources; // resource ID (hash), resource
	private Map<String, List<EventInfo>> resourceEvents; // resource ID, events

	public Server() throws RemoteException {
		super();

		this.registerServerAddress = "";

		this.id = "" + new Date().getTime();
		this.primary = false;

		this.communities = new HashMap<String, CommunityInfo>();
		this.users = new HashMap<String, List<String>>();
		this.events = new HashMap<String, List<EventInfo>>();
		this.resources = new HashMap<String, ResourceInfo>();
		this.resourceEvents = new HashMap<String, List<EventInfo>>();
	}

	/*===================================================================================
	 * Getters and Setters
	===================================================================================*/

	/**
	 * @return the registerServerAddress
	 */
	private String getRegisterServerAddress() {
		return registerServerAddress;
	}

	/**
	 * @param registerServerAddress the registerServerAddress to set
	 */
	private void setRegisterServerAddress(String registerServerAddress) {
		this.registerServerAddress = registerServerAddress;
	}

	/**
	 * @return the id
	 */
	protected String getId() throws RemoteException {
		return id;
	}

	/**
	 * @return the primary
	 */
	public boolean isPrimary() throws RemoteException {
		return primary;
	}

	@Override
	public void setPrimary() throws RemoteException {
		System.out.println("Marked as primary");
		this.primary = true;
	}

	/*===================================================================================
	 * Aux methods
	===================================================================================*/

	private boolean update() throws RemoteException
	{
		boolean updated = false;

		if(this.primary)
		{
			System.out.print("Saving server status... ");
			if(this.writeToFile())
			{
				System.out.println("SUCCESS");
				updated = true;
			}
			else
			{
				System.err.println("ERROR");
				System.err.println("Could not save server status");
			}
		}
		else
		{
			String registerServerAddress = this.registerServerAddress;
			try {
				IRegisterServer registerServer = (IRegisterServer) Naming.lookup("//" +
						registerServerAddress + "/" + REGISTER_SERVER);

				String primaryServerID = registerServer.getPrimaryServerID();
				String primaryServerAddress = registerServer.getPrimaryServerAddress();

				System.out.print("Trying to connect to primary server @ " +
						primaryServerAddress + "... ");
				IServer primaryServer = (IServer) Naming.lookup("//" +
						primaryServerAddress + "/" + primaryServerID);
				System.out.println("SUCCESS");

				System.out.print("Trying to retrieve data from primary server... ");
				byte[] serverData = primaryServer.getServerData();
				Server readServerObject = this.readServerData(serverData);
				System.out.println("SUCCESS");

				if(readServerObject.version > this.version)
				{
					System.out.print("Updating server data from version " +
							this.version + " to version " + readServerObject.version +
							"... ");
					this.communities = readServerObject.communities;
					this.users = readServerObject.users;
					this.events = readServerObject.events;
					this.resources = readServerObject.resources;
					this.resourceEvents = readServerObject.resourceEvents;
					this.version = readServerObject.version;
					System.out.println("SUCCESS");
				}
				else
					System.out.println("Versions match -- no need to update");
				
				updated = true;
				
			} catch (Exception e) {
				System.err.println("ERROR -- Primary server appears to be down");
				System.err.println("Waiting for a new primary server to be appointed");
			}
		}
		return updated;
	}

	@Override
	public boolean ping() throws RemoteException {
		System.out.println("Pinged!");
		return true;
	}

	private static Server readFromFile()
	{
		Server server = null;
		try {
			System.out.print("Trying to load server contents from file... ");

			// open stream from disk using FileInputStream
			FileInputStream f_in = new FileInputStream(SERVER_FILE);
			// open stream from object using ObjectInputStream
			ObjectInputStream obj_in = new ObjectInputStream (f_in);
			// read the actual object
			Object obj = obj_in.readObject();

			if (obj instanceof Server)
				server = (Server) obj;

			System.out.println("SUCCESS");
			
		} catch (FileNotFoundException e) {
			// TODO
		} catch (IOException e) {
			// TODO Auto-generated catch block
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
		}
		return server;
	}

	private boolean writeToFile()
	{
		boolean written = false;
		try {
			System.out.print("Trying to write server contents to file... ");
			
			// set a new version of the server
			this.version = new Date().getTime();

			// open stream to disk with FileOutputStream
			FileOutputStream f_out = new FileOutputStream(SERVER_FILE);
			// open stream to object with ObjectOutputStream
			ObjectOutputStream obj_out = new ObjectOutputStream (f_out);
			// write object out to disk
			obj_out.writeObject ( this );
			
			f_out.flush();
			obj_out.flush();
			f_out.close();
			obj_out.close();
			
			written = true;

			System.out.println("SUCCESS -- wrote version " + this.version);

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return written;
	}

	private Server readServerData(byte[] data)
	{
		Server readServerObject = null;
		try {
			ByteArrayInputStream bis = new ByteArrayInputStream (data);
			ObjectInputStream ois = new ObjectInputStream (bis);
			readServerObject = (Server) ois.readObject();
		}
		catch (IOException ex) {
			ex.printStackTrace();
		}
		catch (ClassNotFoundException ex) {
			ex.printStackTrace();
		}
		return readServerObject;
	}

	@Override
	public byte[] getServerData() throws RemoteException {
		byte[] bytes = null;
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		try {
			ObjectOutputStream oos = new ObjectOutputStream(bos); 
			oos.writeObject(this);
			oos.flush(); 
			oos.close(); 
			bos.close();
			bytes = bos.toByteArray ();
		}
		catch (IOException ex) {
			//TODO: Handle the exception
		}
		return bytes;
	}

	private String getMD5(String string)
	{
		MessageDigest message = null;
		try {
			message = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			// do nothing
		}
		message.update(string.getBytes(),0,string.length());

		String hash = new BigInteger(1,message.digest()).toString(16);

		while (hash.length() < 32)
			hash = "0" + hash;

		return hash;
	}

	/*===================================================================================
	 * Server methods
	===================================================================================*/

	private boolean isMember(String communityName, String client)
	{
		return this.users.get(communityName).contains(client);
	}

	private EventInfo findEvent(String communityName, String eventID)
			throws RemoteException
			{
		EventInfo foundEvent = null;

		for(EventInfo eventInfo : this.listEvents(communityName))
			if(eventInfo.getID().equals(eventID))
				foundEvent = eventInfo;

		return foundEvent;
			}

	@Override
	public boolean addCommunity(String communityName, String pwd) throws RemoteException
	{
		String client = null;
		try {
			client = UnicastRemoteObject.getClientHost();
		} catch (ServerNotActiveException e) {
			System.err.println("ServerNotActiveException while trying to add community");
			e.printStackTrace();
		}

		if(!this.communities.containsKey(communityName))
		{
			this.communities.put(communityName, new CommunityInfo(communityName, pwd));
			List<String> userList = new ArrayList<String>();
			userList.add(client);
			this.users.put(communityName, userList);

			return true;
		}
		return false;
	}

	@Override
	public boolean joinCommunity(String communityName) throws RemoteException
	{
		String client = null;
		try {
			client = UnicastRemoteObject.getClientHost();
		} catch (ServerNotActiveException e) {
			System.err.println("ServerNotActiveException while trying to join community");
			e.printStackTrace();
		}

		// check for community existence
		if(!this.users.containsKey(communityName))
			return false;

		//check for user already present
		if(this.isMember(communityName, client))
			return false;

		return this.users.get(communityName).add(client);
	}

	@Override
	public boolean unJoinCommunity(String communityName) throws RemoteException
	{
		String client = null;
		try {
			client = UnicastRemoteObject.getClientHost();
		} catch (ServerNotActiveException e) {
			System.err.println("ServerNotActiveException while trying to unjoin community");
			e.printStackTrace();
		}

		if(!this.users.containsKey(communityName))
			return false;

		if(this.users.get(communityName).remove(client))
		{
			if(this.users.get(communityName).isEmpty())
			{
				this.events.remove(communityName); // remove community events
				System.out.println("Removed all events of community " + communityName);
				this.users.remove(communityName); // remove community user list
				this.communities.remove(communityName); // remove community
			}
			return true;
		}
		return false;
	}

	@Override
	public List<String> listCommunities() throws RemoteException {
		List<String> communityList = new ArrayList<String>(this.communities.size());
		communityList.addAll(this.communities.keySet());

		return communityList;
	}

	@Override
	public boolean addEvent (EventInfo event) throws RemoteException
	{
		// get community name from the event itself
		String communityName = event.getCommunities().get(0);

		if (!this.events.containsKey(communityName))
		{
			List<EventInfo> eventList = new ArrayList<EventInfo>();
			eventList.add(event);
			this.events.put(communityName, eventList);
		}
		else
			this.events.get(communityName).add(event);

		System.out.println("Event " + event + " added to community " + communityName);

		return true;
	}

	@Override
	public List<EventInfo> listEvents(String communityName) throws RemoteException
	{
		// must be initialized since we can't return null because
		// the interface cant deal with null values, therefore
		// we need to at least return an empty array
		List<EventInfo> eventList = new ArrayList<EventInfo>();

		// no need to do extra processing if the map is empty
		if(!this.events.isEmpty())
			if(communityName.equalsIgnoreCase("all"))
			{
				System.out.println("Listing _all_ events");
				for(List<EventInfo> list : this.events.values())
					for(EventInfo event : list)
						eventList.add(event);
			}
			else
			{
				System.out.println("Listing _all_ events of community: " + communityName);
				// community might exist but have no events
				if(this.events.get(communityName) != null)
					eventList = this.events.get(communityName);
				else
					System.out.println("No events for " + communityName);

			}
		else
			System.out.println("No events at all");

		return eventList;
	}

	@Override
	public List<EventInfo> searchEvents(String communityName, String query)
			throws RemoteException {
		String client = null;
		try {
			client = UnicastRemoteObject.getClientHost();
			System.out.println(client);
		} catch (ServerNotActiveException e) {
			System.err.println("ServerNotActiveException while trying to unjoin community");
			e.printStackTrace();
		}

		// TODO
		//		if(!isMember(communityName, client))
		//			return null; // use null to signal client cannot search this comm

		List<EventInfo> results = new ArrayList<EventInfo>();

		if(communityName.equalsIgnoreCase("all"))
		{
			System.out.println("Searching for " + query + " in _all_ events");
			for(List<EventInfo> list : this.events.values())
				for(EventInfo event : list)
				{
					String eventName = event.getName().toLowerCase();
					String eventDescription = event.getDescription().toLowerCase();

					if(eventName.contains(query.toLowerCase()) ||
							eventDescription.contains(query.toLowerCase()))
						results.add(event);
				}
		}
		else
		{
			System.out.println("Searching for " + query + " in " +
					communityName + " events on this server");
			for(EventInfo event : this.events.get(communityName))
			{
				String eventName = event.getName().toLowerCase();
				String eventDescription = event.getDescription().toLowerCase();

				if(eventName.contains(query.toLowerCase()) ||
						eventDescription.contains(query.toLowerCase()))
					results.add(event);
			}
		}
		return results;
	}

	// assumes the event already exists
	@Override
	public boolean addResource(String communityName, String eventID,
			ResourceInfo resource)
					throws RemoteException
					{
		ResourceInfo newResource = resource;
		String resourceIDHash = this.getMD5(newResource.getID());

		EventInfo serverEvent = this.findEvent(communityName, eventID);

		if(this.resources.containsKey(resourceIDHash))
		{
			System.out.println("Already existing resource: " + resourceIDHash);
			// the only difference that can happen here is the new resource
			// is now local because some host decided to re-import & copy it
			if(newResource.isLocal() != this.resources.get(resourceIDHash).isLocal())
			{
				System.out.println("Resource " + resourceIDHash +
						" has become local");
				// prioritize local resources
				ResourceInfo oldResource = this.resources.put(resourceIDHash, newResource);
				// update all events containing this resource
				for(EventInfo event : this.resourceEvents.get(resourceIDHash))
				{
					int index = event.getResourceInfo().indexOf(oldResource);
					event.getResourceInfo().set(index, newResource);
				}
			}
			// get the resource from the hashmap
			newResource = this.resources.get(resourceIDHash);
		}
		else
		{
			// entirely new resource
			this.resources.put(resourceIDHash, newResource);

			// guaranteed _not_ to exist any event containing this new resource
			this.resourceEvents.put(resourceIDHash, new ArrayList<EventInfo>());
		}

		if(serverEvent.getResourceInfo().contains(newResource))
		{
			System.out.println("Event " + serverEvent.getID() +
					" already contains this resource");
			return false;
		}
		else
		{
			this.resourceEvents.get(resourceIDHash).add(serverEvent);
			System.out.println("Associated event " + serverEvent.getID() +
					" with resource " + resourceIDHash);
		}

		System.out.println("Added resource " + newResource +
				" with hash " + resourceIDHash + " to server");
		
		return serverEvent.getResourceInfo().add(newResource);
					}

	/**
	 * Finds _all_ events with the resource and adds the calling
	 * client's address to the list of urls containing the resource.
	 * Assumes the resource already exists.
	 */
	@Override
	public boolean addResourceProvider(String resourceID)
			throws RemoteException
			{
		ResourceInfo serverResource = this.resources.get(resourceID);

		String remoteAddress = null;
		try {
			remoteAddress = UnicastRemoteObject.getClientHost();
		} catch (ServerNotActiveException e) {
			System.err.println("Error getting client address");
			return false;
		}

		for(String url : serverResource.getURL())
			if(url.equals(remoteAddress))
			{
				System.out.println("Already provider: " + remoteAddress);
				return false;
			}
		System.out.println("New provider: " + remoteAddress);
		return serverResource.getURL().add(remoteAddress);
			}

	/*===================================================================================
	 * Main
	===================================================================================*/

	public static void main(String args[]) {

		Server server = null;
		String serverID = null;
		// bind this server on the local registry
		try {
			System.getProperties().put( "java.security.policy", "src/servers/policy.all");

			KeyManager.getInstance();
			KeyManager.makePublicCert("server");
			
			if( System.getSecurityManager() == null) {
				System.setSecurityManager( new RMISecurityManager());
			}

			try { // start rmiregistry
				LocateRegistry.createRegistry( 1099);
			} catch( RemoteException e) { // if not start it
				// do nothing : already started with 'rmiregistry'
			}

			server = new Server();
			serverID = server.getId();
			
			RMIClientSocketFactory csf = new CipherClientSocketFactory();
	        RMIServerSocketFactory ssf = new CipherServerSocketFactory();
	        IServer stub = (IServer) UnicastRemoteObject.exportObject(server, 0, csf, ssf);

			Naming.rebind(serverID, stub);
			System.out.println( "Server " + serverID + " bound in registry");

		} catch( Throwable th) {
			th.printStackTrace();
		}

		// register on the RegisterServer
		boolean registered = false;
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

		while(!registered)
		{
			System.out.println("\nPlease type in the address of the register server: " +
					"(leave blank for localhost)");
			String registerServerAddress;
			try {
				server.setRegisterServerAddress(reader.readLine());
			} catch (IOException e) {
				e.printStackTrace();
			}
			
			registerServerAddress = server.getRegisterServerAddress();

			// simple IPv4 regexp for validation
			if(registerServerAddress.isEmpty() ||
					registerServerAddress.matches("^(\\d{1,3}\\.){3}(\\d{1,3})$"))
			{
				if(registerServerAddress.isEmpty())
					registerServerAddress = "localhost";

				System.out.println("Acknowledging " + registerServerAddress + 
						" as the location of the register server");
				System.out.print("Trying to register this server... ");

				IRegisterServer registerServer;
				try {
					registerServer = (IRegisterServer) Naming.lookup("//" +
							registerServerAddress + "/" + REGISTER_SERVER);

					if(registerServer.registerServer(serverID))
					{
						registered = true;
						System.out.println("SUCCESS");

						if(server.isPrimary())
						{
							Server loadedServer = readFromFile();
							if(loadedServer != null)
							{
								//server = loadedServer; // won't work
								server.communities = loadedServer.communities;
								server.users = loadedServer.users;
								server.events = loadedServer.events;
								server.resources = loadedServer.resources;
								server.resourceEvents = loadedServer.resourceEvents;
								server.version = loadedServer.version;
							}
							else
							{
								System.err.println("ERROR -- " +
										"Failed to load primary " +
										"server state, starting over");
							}
						}
						else
							server.update();
						
						Thread r2d2 = new Thread(new Maintenance(server, server.getRegisterServerAddress()));
						r2d2.start();
					}
					else
					{
						System.err.println("ERROR\n");
						System.out.println("Could not register this server");
						System.out.println("------------------------------");
					}
				} catch (MalformedURLException e) {
					System.err.println("ERROR\n");
					System.out.println("Register server: Bad URL");
				} catch (RemoteException e) {
					System.err.println("ERROR\n");
					System.out.println("Register server: " +
							"No registry found @ " + registerServerAddress);
					e.printStackTrace();
				} catch (NotBoundException e) {
					System.err.println("ERROR\n");
					System.out.println("Register server not bound in the " +
							"registry @ " + registerServerAddress);
				}
			}
			else
			{
				System.out.println("Not a valid IP address format");
				System.out.println("------------------------------");
			}
		}
	}

	class CommunityInfo implements Serializable {

		private static final long serialVersionUID = 1L;
		
		String name;
		String pwd;
		String url;
		public CommunityInfo(String name, String pwd, String url) {
			this( name, pwd);
			this.url = url;
		}
		public CommunityInfo(String name, String pwd) {
			this.name = name;
			this.pwd = pwd;
		}

	}
	
	private static class Maintenance implements Runnable
	{
		private static final long SLEEP_TIME_PRIMARY = 60000;
		private static final long SLEEP_TIME_SECONDARY = 120000;
		
		private Server server;
		private String registerServerAddress;
		
		public Maintenance(Server server, String registerServerAddress)
		{
			this.server = server;
			this.registerServerAddress = registerServerAddress;
		}
		@Override
		public void run() {
			System.out.println("R2-D2 Thread Initialized");
			for(;;)
			{
				try {
					if(server.isPrimary())
					{
						Thread.sleep(SLEEP_TIME_PRIMARY);
						IRegisterServer registerServer = (IRegisterServer) Naming.lookup("//" +
								this.registerServerAddress + "/" + REGISTER_SERVER);
						registerServer.setLease(new Date().getTime());
					}
					else
						Thread.sleep(SLEEP_TIME_SECONDARY);
					
					this.server.update(); // whether primary or seconday, update
					
				} catch (InterruptedException e) {
					System.err.println("Thread interrupted");
					e.printStackTrace();
				} catch (MalformedURLException e) {
					e.printStackTrace();
					break;
				} catch (RemoteException e) {
					e.printStackTrace();
					break;
				} catch (NotBoundException e) {
					e.printStackTrace();
					break;
				}
			}
		}
	}
}


